/*
 * Copyright (C) 2009, Edmundo Albuquerque de Souza e Silva.
 *
 * This file may be distributed under the terms of the Q Public License
 * as defined by Trolltech AS of Norway and appearing in the file
 * LICENSE.QPL included in the packaging of this file.
 *
 * THIS FILE IS PROVIDED AS IS WITH NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

/*
 * Copyright (C) 2009, Edmundo Albuquerque de Souza e Silva.
 *
 * This file may be distributed under the terms of the Q Public License
 * as defined by Trolltech AS of Norway and appearing in the file
 * LICENSE.QPL included in the packaging of this file.
 *
 * THIS FILE IS PROVIDED AS IS WITH NO WARRANTY OF ANY KIND, INCLUDING
 * THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
 * INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
 * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
 * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

package net.sf.fmj.ffmpeg_java;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;
import javax.media.protocol.Seekable;
import javax.media.protocol.SourceCloneable;

import net.sf.ffmpeg_java.AVFormatLibrary;
import net.sf.ffmpeg_java.AVFormatLibrary.URLContext;
import net.sf.ffmpeg_java.custom_protocol.CallbackURLProtocolHandler;
import net.sf.fmj.utility.LoggerSingleton;

import com.sun.jna.Pointer;

/**
 * Implements CallbackURLProtocolHandler by using a PullSourceStream.
 * 
 * Because seeking functions have to be implemented, either the source has to be Cloneable, or the
 * streams provided by the source have to be Seekable.  Otherwise, seeking will fail, and
 * ffmpeg does not really check for seek failures, it mostly just assumes they will work.
 * This was written with an eye on ffmpeg's http.c, which shows that when a seek is needed, they simply
 * open a new stream. http.c is not of course used when this is used, it just shows what kind of 
 * behavior is expected from a URLProtocol.
 * 
 * 
 *
 */
public class PullDataSourceCallbackURLProtocolHandler implements CallbackURLProtocolHandler
{
	private static final Logger logger = LoggerSingleton.logger;

	private PullDataSource source;
	private PullSourceStream pss;
	private long curpos;	// for seeking
	
	public static final boolean TRACE = false;
	private boolean mustClone = false;
	private final boolean isSourceCloneable;
	
	public PullDataSourceCallbackURLProtocolHandler(PullDataSource source)
	{
		super();
		this.source = source;
		isSourceCloneable = source instanceof SourceCloneable;
		
	}

	public int open(URLContext h, String filename, int flags)
	{
		if (TRACE) System.out.print("open: flags=" + flags);
		
		if ((flags & AVFormatLibrary.URL_RDWR) != 0)
		{	
			if (TRACE) System.out.println(" return " + -1);
			logger.severe("PullSourceStreamCallbackURLProtocolHandler: only read-only open supported");
			return -1;
		}
		else if ((flags & AVFormatLibrary.URL_WRONLY) != 0)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.severe("PullSourceStreamCallbackURLProtocolHandler: only read-only open supported");
			return -1;
		}

		try
		{
			doOpen();
		}
		catch (IOException e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.WARNING, "" + e, e);
			return -1;
		}
		catch (Throwable e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.SEVERE, "" + e, e);
			return -1;
		}
		
		if (TRACE) System.out.println(" return " + 0);
		return 0;
	}
	
	private boolean opened = false;
	private void doOpen() throws IOException
	{
		if (opened)
			return;
		
		if (mustClone)
		{	if (!isSourceCloneable)
			{
				logger.severe("PullSourceStreamCallbackURLProtocolHandler: cannot reopen because source is not SourceCloneable");
				throw new IOException("not SourceCloneable");
			}
			source = (PullDataSource) ((SourceCloneable) source).createClone();
		}

		source.connect();	// TODO: we don't need to connect if already connected
		source.start();

		final PullSourceStream[] pullSourceStreams = source.getStreams();
		if (pullSourceStreams.length < 1)
		{	logger.warning("No streams");
			throw new IOException("No streams");
		}
		
		pss = pullSourceStreams[0];
		
		curpos = 0;
		opened = true;
	}
	
	private void closeAndReopen() throws IOException
	{
		doClose();
		doOpen();
	}
	

	public int read(URLContext h, Pointer buf, int size)
	{
		if (TRACE) System.out.print("read: size=" + size);
		
		// TODO: is there a way to do this without having a copy of the byte array?
		final byte[] ba = new byte[size]; //buf.getByteArray(0, size);
		
		try
		{
			if (!opened)
			{
				logger.warning("Attempt to read with closed stream");
				if (TRACE) System.out.println(" return " + -1);
				return -1;
			}
			
			final int ret = pss.read(ba, 0, size);
			buf.write(0, ba, 0, size);
			curpos += ret;
			
			if (TRACE) System.out.println(" return " + ret);
			return ret;
		} 
		catch (IOException e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.WARNING, "" + e, e);
			return -1;
		}
		catch (Throwable e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.SEVERE, "" + e, e);
			return -1;
		}

	}
	
	public int write(URLContext h, Pointer buf, int size)
	{
		if (TRACE) System.out.print("write: size=" + size);
		
		logger.severe("write not supported");
		return -1;
		
	}

	public long seek(URLContext h, long pos, int whence)
	{
		if (TRACE) System.out.print("seek: pos=" + pos + " whence=" + whence);
		
		// doesn't need to be seekable to query the size:
		if (whence == AVFormatLibrary.AVSEEK_SIZE)
		{
			final long ret = pss.getContentLength();
			if (TRACE) System.out.println(" return " + ret);
			return ret;
		}
		
		if (whence == SEEK_END)
		{	
			// cannot seek relative to end if no known content length.
			if (pss.getContentLength() < 0)
			{	
				if (TRACE) System.out.println(" return " + -1);
				return -1;	
			}
		}
		
		final boolean isSeekable = pss instanceof Seekable;
		
		try
		{
			if (!isSeekable)
			{
				// if the stream is not seekable, we have to seek by reading/ignoring and/or closing/reopening the stream to get to zero.
				
				if (whence == SEEK_END)
				{
					// seek relative to the end - translate this into an absolute seek
					whence = SEEK_SET;
					pos += pss.getContentLength();
				}
				
				// at this point whence is either SEEK_SET or SEEK_CUR
				
				if (whence == SEEK_CUR)
				{	
					// relative seek: translate to absolute seek
					whence = SEEK_SET;
					pos += curpos;
				}
				
				// at this point whence is definitely SEEK_SET (or an invalid value)
				
				if (whence == SEEK_SET) 
				{	
					if (pss.getContentLength() > 0 && pos >= pss.getContentLength())
					{
						if (TRACE) System.out.println("Seek to end: closing...");
						// seek to end or past.  No need to read everything, just close stream and update pos.
						// subsequent seeks will cause us to reopen.
						doClose();
						curpos = pos;
						return curpos;
						
					}
					else if (pos >= curpos)
					{	// absolute seek to after our position: simply do a SEEK_CUR below.
						whence = SEEK_CUR;
						pos -= curpos;
					}
					else
					{
						// absolute seek to something before where we are: translate to relative seek to zero, and close and reopen stream, so that we are back at zero.
						whence = SEEK_CUR;
						if (TRACE) System.out.println("Closing and reopening...");
						closeAndReopen();
					}
				}	
				
				if (whence == SEEK_CUR)
				{	// implement seek by repeated read:
					final byte[] b = new byte[1];
					
					// TODO: very inefficient
					// TODO: we don't actually have to do the read until later, if we don't want to.
					for (int i = 0; i < pos; ++i)
					{	if (pss.read(b, 0, 1) < 0)	
						{	
							// end of stream - TODO: is this what we are supposed to do?
							logger.warning("attempt to seek past end of stream");
							if (TRACE) System.out.println(" return " + -1);
							return -1;
						}	
						curpos += 1;
					}	
					return curpos;
				}
			}
			
			if (!(isSeekable))
			{	
				if (TRACE) System.out.println(" return " + -1);
				//System.err.println("pss not Seekable");
				return -1;
			}
			
			final Seekable seekable = (Seekable) pss;
		

			final long seekTo;
			if (whence == SEEK_SET)
				seekTo = pos;
			else if (whence == SEEK_CUR)
				seekTo = curpos + pos;
			else if (whence == SEEK_END)
			{	if (pss.getContentLength() < 0)
					return -1;	
				seekTo = pss.getContentLength() + pos;
			}
			else
			{	if (TRACE) System.out.println(" return " + -1);
				logger.warning("seek: Invalid whence value: " + whence);
				return -1;
			}
			
			final long ret = seekable.seek(seekTo);
			curpos = ret;
			if (TRACE) System.out.println(" return " + ret);
			return ret;
		}
		catch (IOException e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.WARNING, "" + e, e);
			return -1;
		}
		catch (Throwable e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.SEVERE, "" + e, e);
			return -1;
		}

	}
	
	public int close(URLContext h)
	{
		// TODO: it is not clear that we should really do the close here, if the source would be closed elsewhere.
		if (TRACE) System.out.print("close");
		
		try
		{
			doClose();
		}
		catch (IOException e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.WARNING, "" + e, e);
			return -1;
		}
		catch (Throwable e)
		{
			if (TRACE) System.out.println(" return " + -1);
			logger.log(Level.SEVERE, "" + e, e);
			return -1;
		}

		
		
		if (TRACE) System.out.println(" return " + 0);
		return 0;
	}
	
	private void doClose() throws IOException
	{
		if (!opened)
			return;
		try
		{
			source.stop();
			source.disconnect();
		}
		finally
		{
			mustClone = true;
			opened = false;
		}
	}



}
